嗨大家,我是 Debuguy。
今天要來聊一個很現實的話題:有時候 Prompt Engineering 真的不是萬能的。
最一開始這個系列文章就有說到我希望的是之後會有 Slack AI Agent 來幫我找問題甚至解決問題
前面讓 Slack 可以像 ChatBot 已經花不少時間接下來
想說接個 Elasticsearch MCP 應該就能搞定 ... but!!!事情果然沒有想像中的簡單
想要讓 ChatBot 能查 Elasticsearch,第一件事就是把 Elasticsearch MCP 接上去。這步驟看起來很直觀:
# docker-compose.yml
services:
elasticsearch-mcp:
image: docker.elastic.co/mcp/elasticsearch
environment:
- ES_URL=${ES_URL}
- ES_API_KEY=${ES_API_KEY}
ports:
- "8080:8080"
command: ["http"]
restart: unless-stopped
// config/mcp-config.json
{
"mcpServers": {
"elasticsearch": {
"type": "streamable-http",
"url": "http://elasticsearch-mcp:8080/mcp"
}
}
}
就這樣,理論上 ChatBot 就有了查詢 Elasticsearch 的能力了!
「看吧,很簡單嘛!現在只要告訴 AI 怎麼用就好了。」
但現實很快就告訴我事情沒這麼簡單...
公司的 Alert 系統會發出 Kibana 的 URL,像這樣:
🚨 Error Alert 🚨
Production API 出現異常
查看詳細 Log: https://demo.elastic.co/app/discover#/?_g=(filters:!(),refreshInterval:(pause:!t,value:60000),time:(from:'2025-10-04T14:30:00.000Z',to:'2025-10-04T15:00:00.000Z'))&_a=(columns:!(),dataSource:(dataViewId:'filebeat-*',type:dataView),filters:!(),interval:auto,query:(language:kuery,query:'log.level:%20%22error%22%20'),sort:!(!('@timestamp',desc)))
@debuguy_bot 幫我看一下這次的錯誤 Log
我想要的自動化流程是:
「這應該很簡單吧?不就是加個 Elasticsearch MCP 工具而已嗎?」
一開始,我在 System Prompt 裡給了 URL pattern 和 DSL template:
## Kibana URL Pattern Recognition
When you see a URL like this:
https://demo.elastic.co/app/discover#/?_g=(filters:!(),refreshInterval:(pause:!t,value:60000),time:(from:'2025-10-04T14:30:00.000Z',to:'2025-10-04T15:00:00.000Z'))&_a=(columns:!(),dataSource:(dataViewId:'filebeat-*',type:dataView),filters:!(),interval:auto,query:(language:kuery,query:'log.level:%20%22error%22%20'),sort:!(!('@timestamp',desc)))
Generate Elasticsearch DSL using this template:
{
"query":{
"bool":{
"filter":[
{
"bool":{
"should":[
{
"term":{
"log.level":{
"value":"error"
}
}
}
]
}
},
{
"range":{
"@timestamp":{
"format":"strict_date_optional_time",
"gte":"2025-10-04T14:30:00.000Z",
"lte":"2025-10-04T15:00:00.000Z"
}
}
}
]
}
},
"size":100
}
然後很期待地測試...
結果 AI 很常寫錯 query 語法
我仔細看了 Elasticsearch MCP 的 JSON Schema:
{
"name": "search",
"inputSchema": {
"properties": {
"query_body": {
"additionalProperties": true,
"description": "Complete Elasticsearch query DSL object",
"type": "object"
}
}
}
}
發現問題了!
query_body
只是一個 "type": "object"
加上 "additionalProperties": true
,完全沒有結構化的 Schema!
LLM 要怎麼知道:
query_body
裡面應該放什麼?看起來很嚇人對吧?讓我們一步步拆解:
基礎 URL: https://demo.elastic.co/app/discover
Hash Fragment: #/?_g=...&_a=...
Kibana 把所有的狀態資訊都塞在 URL 的 hash fragment 裡面。
_g: 全域狀態 (Global State)
_a: 應用狀態 (App State)
_g
包含時間範圍、刷新間隔等全域設定_a
包含查詢、過濾器、排序等應用特定設定這些參數使用一種叫做 Rison 的格式編碼,這是 JSON 的一種 URL-safe 變體:
這裡有個線上工具大家可以玩看看
// _g 的 Rison 編碼
(filters:!(),refreshInterval:(pause:!t,value:60000),time:(from:'2025-10-04T14:30:00.000Z',to:'2025-10-04T15:00:00.000Z'))
// _g 解碼後的 JSON
{
"filters": [],
"refreshInterval": {
"pause": true,
"value": 60000
},
"time": {
"from": "2025-10-04T14:30:00.000Z",
"to": "2025-10-04T15:00:00.000Z"
}
}
// _a 的 Rison 編碼
(columns:!(),dataSource:(dataViewId:'filebeat-*',type:dataView),filters:!(),interval:auto,query:(language:kuery,query:'log.level:%20%22error%22%20'),sort:!(!('@timestamp',desc)))
// _a 解碼後的 JSON
{
"columns": [],
"dataSource": {
"dataViewId": "filebeat-*",
"type": "dataView"
},
"filters": [],
"interval": "auto",
"query": {
"language": "kuery",
"query": "log.level:%20%22error%22%20"
},
"sort": [
[
"@timestamp",
"desc"
]
]
}
// _a 解碼後
{
"query": {
"language": "kuery",
"query": "log.level:%20%22error%22%20"
},
}
這個 query 用 url decode 之後是log.level: "error"
也就是 kurey: kibana 上的查詢語法
最複雜的部分是把 KQL (Kuery) 轉換成 Elasticsearch DSL:
// KQL
"log.level: \"error\""
// 需要轉換成 Elasticsearch DSL
{
"bool":{
"filter":[
{
"bool":{
"should":[
{
"term":{
"log.level":{
"value":"error"
}
}
}
]
}
}
]
}
}
Kibana URL
↓ 解析 hash fragment
URL Parameters (_g, _a)
↓ Rison decode
JSON Objects
↓ 提取時間範圍 (_g.time)
Time Range Filter
↓ 提取查詢條件 (_a.query)
KQL Query String
↓ KQL → DSL 轉換
Elasticsearch Query DSL
↓ 組合完整查詢
Final Query with filters, time range, sorting
這麼多步驟,每一步都有出錯的可能,難怪 LLM 搞不定!
我突然想通了:
與其花時間教 AI 怎麼轉換格式,不如直接寫個工具幫它轉換好。
這就像翻譯蒟蒻,讓它可以直接理解外星語言,而不是教它外星語的文法。
// GenKit/tools/kibana_tools.ts
export const createKibanaUrlToElasticQueryDslTool = (ai: Genkit) => {
return ai.dynamicTool({
name: 'kibana_url_to_elastic_query_dsl',
description: 'Convert Kibana URL to Elasticsearch Query DSL. Parses Kibana discover URL and extracts query parameters, filters, and time range to generate corresponding Elasticsearch query.',
inputSchema: z.object({
kibanaUrl: z.string().describe('The Kibana discover URL containing query parameters')
}),
outputSchema: z.object({
queryDsl: z.record(z.unknown()).describe('The generated Elasticsearch Query DSL object'),
extractedParams: z.object({
timeRange: z.object({
from: z.string(),
to: z.string()
}).optional(),
query: z.record(z.unknown()).optional(),
filters: z.array(z.record(z.unknown())).optional()
}),
original: z.string().describe('The original Kibana URL')
})
}, async ({ kibanaUrl }) => {
// 1. 解析 URL hash fragment
const parsed = new URL(kibanaUrl);
const hash = parsed.hash.substring(2); // 移除 '#/'
const params = new URLSearchParams(hash);
// 2. 解碼 rison 參數
const _g = rison.decode(decodeURIComponent(params.get('_g')!));
const _a = rison.decode(decodeURIComponent(params.get('_a')!));
// 3. 建立時間範圍 filter
const timeFilter = {
range: {
"@timestamp": {
gte: _g.time.from,
lte: _g.time.to,
format: "strict_date_optional_time"
}
}
};
// 4. 使用 @vartoso/kql-to-dsl 轉換 KQL 到 DSL
const queryDsl = buildEsQuery(undefined, _a.query, _a.filters, undefined);
return {
queryDsl: {
query: queryDsl,
size: 100,
sort: [{ "@timestamp": { order: "asc" } }]
},
extractedParams: {
timeRange: _g.time,
query: _a.query,
filters: _a.filters
},
original: parsed.pathname + parsed.hash
};
});
};
如果需要測試轉換的語法可以使用 Kibana 提供的 Dev Tools
Rison: Kibana 用來在 URL 中編碼 JSON 的格式,使用 rison-node
套件
KQL 轉 DSL: 使用 @vartoso/kql-to-dsl
套件
現在的流程變成:
User: 幫我查這個 Kibana URL 的資料
↓
Bot: 使用 kibana_url_to_elastic_query_dsl 工具
↓
工具自動轉換為正確的 Elasticsearch DSL
↓
Bot: 使用 elasticsearch search 工具查詢
↓
回傳查詢結果!
最重要的是,工具的 description
本身就告訴了 LLM 該怎麼使用:
Convert Kibana URL to Elasticsearch Query DSL. Parses Kibana discover URL and extracts query parameters, filters, and time range to generate corresponding Elasticsearch query.
LLM 看到 Kibana URL 就知道:
不需要複雜的 System Prompt,工具描述就夠了!
Prompt Engineering 很強大,但不是萬能的。當你發現自己在 Prompt 裡寫了太多「演算法」時,也許該考慮寫程式了。
適合 Prompt 的:
不適合 Prompt 的:
好的工具設計應該:
這個決定其實影響了整個系統的架構:
從「教 AI 做事」變成「給 AI 工具」
原本我想透過 Prompt 教 LLM 各種技能,現在我提供工具讓 LLM 呼叫專業服務。這樣的架構:
最好的 AI 產品不是讓 AI 做所有事,而是讓 AI 和程式各自做最擅長的事。
AI 負責:
Code 負責:
當我們放下「用 Prompt 解決一切」的執著,反而創造出更強大、更穩定的系統。
完整的原始碼在這裡
AI 的發展變化很快,目前這個想法以及專案也還在實驗中。但也許透過這個過程大家可以有一些經驗和想法互相交流,歡迎大家追蹤這個系列。
也歡迎追蹤我的 Threads @debuguy.dev